import string
from django.core.exceptions import ValidationError
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from typing import Any, Dict, List, Union


def create_schema(schema: str) -> None:
    """
    Creates a PostgreSQL database schema
    """
    from django.db import connection
    with connection.cursor() as cursor:
        cursor.execute(f'CREATE SCHEMA {schema};')


def schema_exists(schema: str) -> bool:
    """
    Checks whether a PostgreSQL database schema already exists
    """
    from django.db import connection
    exists = False
    with connection.cursor() as cursor:

        # Query PG database server for schema existence
        sql = 'SELECT EXISTS(SELECT 1 FROM pg_catalog.pg_namespace WHERE LOWER(nspname) = LOWER(%s))'
        cursor.execute(sql, (schema, ))

        # Send query and determine
        row = cursor.fetchone()[0]
        exists = bool(row)
    return exists


def is_valid_pgsql_schema_name(text: str):
    """
    Quote document:
    https://www.postgresql.org/docs/9.2/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS

    SQL identifiers and key words must begin with a letter (a-z, but also
    letters with diacritical marks and non-Latin letters) or an underscore (_).
    Subsequent characters in an identifier or key word can be letters, underscores,
    digits (0-9), or dollar signs ($). Note that dollar signs are not allowed in identifiers
    according to the letter of the SQL standard.
    """
    if not isinstance(text, str) or not text:
        raise ValidationError(f'{text} is not a non-empty string')

    allowed_first_char = string.ascii_letters + '_'
    allowed_letters = string.digits + allowed_first_char

    is_valid_first = text[0] in allowed_first_char
    is_valid_text = all(char in allowed_letters for char in text)

    if not (is_valid_first and is_valid_text):
        raise ValidationError(f'{text} is not a valid pgsql schema name')

    if 'public' in text:
        raise ValidationError('"public" is s reserved schema name part. please use something else')


def force_list(obj: Any) -> List[Any]:
    if not isinstance(obj, list):
        return [obj]
    return obj


def get_model_permissions(models: Union[Any, List[Any]], permission_type: str) -> List[Permission]:
    """
    Retrieve native Django permission objecets of the requested model(s)

    Args:
        model (Union[Any, List[Any]]): models to retrieve permission objects for
        permission_type (str): select from ('read', 'write', 'all', 'add', 'change', 'delete')
    """

    assert permission_type in ('read', 'write', 'all', 'add', 'change', 'delete')

    models = force_list(models)
    content_types = [x for _, x in ContentType.objects.get_for_models(*models).items()]

    filter_kwargs: Dict[str, Any] = {
        'content_type__in': content_types
    }
    if permission_type == 'read':
        filter_kwargs['codename__startswith'] = 'view'
    elif permission_type == 'write':
        filter_kwargs['codename__iregex'] = r'^add|change|delete'
    elif permission_type == 'all':
        # No restriction
        pass
    else:
        filter_kwargs['codename__iregex'] = f'^{permission_type}'

    return list(Permission.objects.filter(**filter_kwargs))
